Skip to content

Commit d1fb6f3

Browse files
authored
Merge pull request #20 from nblumhardt/f-flushtodisk
Flush to disk option
2 parents 3384cde + 5fd5494 commit d1fb6f3

11 files changed

+192
-42
lines changed

src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs

+16-6
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public static class FileLoggerConfigurationExtensions
4848
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
4949
/// is false.</param>
5050
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
51+
/// <param name="flushToDiskInterval">If provided, a full disk flush will be performed periodically at the specified interval.</param>
5152
/// <returns>Configuration object allowing method chaining.</returns>
5253
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
5354
public static LoggerConfiguration File(
@@ -59,14 +60,15 @@ public static LoggerConfiguration File(
5960
long? fileSizeLimitBytes = DefaultFileSizeLimitBytes,
6061
LoggingLevelSwitch levelSwitch = null,
6162
bool buffered = false,
62-
bool shared = false)
63+
bool shared = false,
64+
TimeSpan? flushToDiskInterval = null)
6365
{
6466
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
6567
if (path == null) throw new ArgumentNullException(nameof(path));
6668
if (outputTemplate == null) throw new ArgumentNullException(nameof(outputTemplate));
6769

6870
var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider);
69-
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared);
71+
return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval);
7072
}
7173

7274
/// <summary>
@@ -75,7 +77,7 @@ public static LoggerConfiguration File(
7577
/// <param name="sinkConfiguration">Logger sink configuration.</param>
7678
/// <param name="formatter">A formatter, such as <see cref="JsonFormatter"/>, to convert the log events into
7779
/// text for the file. If control of regular text formatting is required, use the other
78-
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool)"/>
80+
/// overload of <see cref="File(LoggerSinkConfiguration, string, LogEventLevel, string, IFormatProvider, long?, LoggingLevelSwitch, bool, bool, TimeSpan?)"/>
7981
/// and specify the outputTemplate parameter instead.
8082
/// </param>
8183
/// <param name="path">Path to the file.</param>
@@ -89,6 +91,7 @@ public static LoggerConfiguration File(
8991
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
9092
/// is false.</param>
9193
/// <param name="shared">Allow the log file to be shared by multiple processes. The default is false.</param>
94+
/// <param name="flushToDiskInterval">If provided, a full disk flush will be performed periodically at the specified interval.</param>
9295
/// <returns>Configuration object allowing method chaining.</returns>
9396
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
9497
public static LoggerConfiguration File(
@@ -99,9 +102,10 @@ public static LoggerConfiguration File(
99102
long? fileSizeLimitBytes = DefaultFileSizeLimitBytes,
100103
LoggingLevelSwitch levelSwitch = null,
101104
bool buffered = false,
102-
bool shared = false)
105+
bool shared = false,
106+
TimeSpan? flushToDiskInterval = null)
103107
{
104-
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared);
108+
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered: buffered, shared: shared, flushToDiskInterval: flushToDiskInterval);
105109
}
106110

107111
/// <summary>
@@ -169,7 +173,8 @@ static LoggerConfiguration ConfigureFile(
169173
LoggingLevelSwitch levelSwitch = null,
170174
bool buffered = false,
171175
bool propagateExceptions = false,
172-
bool shared = false)
176+
bool shared = false,
177+
TimeSpan? flushToDiskInterval = null)
173178
{
174179
if (addSink == null) throw new ArgumentNullException(nameof(addSink));
175180
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
@@ -212,6 +217,11 @@ static LoggerConfiguration ConfigureFile(
212217
return addSink(new NullSink(), LevelAlias.Maximum, null);
213218
}
214219

220+
if (flushToDiskInterval.HasValue)
221+
{
222+
sink = new PeriodicFlushToDiskSink(sink, flushToDiskInterval.Value);
223+
}
224+
215225
return addSink(sink, restrictedToMinimumLevel, levelSwitch);
216226
}
217227
}

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

+16-8
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ namespace Serilog.Sinks.File
2424
/// <summary>
2525
/// Write log events to a disk file.
2626
/// </summary>
27-
public sealed class FileSink : ILogEventSink, IDisposable
27+
public sealed class FileSink : ILogEventSink, IFlushableFileSink, IDisposable
2828
{
2929
readonly TextWriter _output;
30+
readonly FileStream _underlyingStream;
3031
readonly ITextFormatter _textFormatter;
3132
readonly long? _fileSizeLimitBytes;
3233
readonly bool _buffered;
@@ -61,13 +62,13 @@ public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBy
6162
Directory.CreateDirectory(directory);
6263
}
6364

64-
Stream file = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read);
65+
Stream outputStream = _underlyingStream = System.IO.File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read);
6566
if (_fileSizeLimitBytes != null)
6667
{
67-
file = _countingStreamWrapper = new WriteCountingStream(file);
68+
outputStream = _countingStreamWrapper = new WriteCountingStream(_underlyingStream);
6869
}
6970

70-
_output = new StreamWriter(file, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
71+
_output = new StreamWriter(outputStream, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
7172
}
7273

7374
/// <summary>
@@ -91,10 +92,17 @@ public void Emit(LogEvent logEvent)
9192
}
9293
}
9394

94-
/// <summary>
95-
/// Performs application-defined tasks associated with freeing, releasing, or
96-
/// resetting unmanaged resources.
97-
/// </summary>
95+
/// <inheritdoc />
9896
public void Dispose() => _output.Dispose();
97+
98+
/// <inheritdoc />
99+
public void FlushToDisk()
100+
{
101+
lock (_syncRoot)
102+
{
103+
_output.Flush();
104+
_underlyingStream.Flush(true);
105+
}
106+
}
99107
}
100108
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Serilog.Sinks.File
2+
{
3+
/// <summary>
4+
/// Supported by (file-based) sinks that can be explicitly flushed.
5+
/// </summary>
6+
public interface IFlushableFileSink
7+
{
8+
/// <summary>
9+
/// Flush buffered contents to disk.
10+
/// </summary>
11+
void FlushToDisk();
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Threading;
3+
using Serilog.Core;
4+
using Serilog.Debugging;
5+
using Serilog.Events;
6+
7+
namespace Serilog.Sinks.File
8+
{
9+
/// <summary>
10+
/// A sink wrapper that periodically flushes the wrapped sink to disk.
11+
/// </summary>
12+
public class PeriodicFlushToDiskSink : ILogEventSink, IDisposable
13+
{
14+
readonly ILogEventSink _sink;
15+
readonly Timer _timer;
16+
int _flushRequired;
17+
18+
/// <summary>
19+
/// Construct a <see cref="PeriodicFlushToDiskSink"/> that wraps
20+
/// <paramref name="sink"/> and flushes it at the specified <paramref name="flushInterval"/>.
21+
/// </summary>
22+
/// <param name="sink">The sink to wrap.</param>
23+
/// <param name="flushInterval">The interval at which to flush the underlying sink.</param>
24+
/// <exception cref="ArgumentNullException"></exception>
25+
public PeriodicFlushToDiskSink(ILogEventSink sink, TimeSpan flushInterval)
26+
{
27+
if (sink == null) throw new ArgumentNullException(nameof(sink));
28+
29+
_sink = sink;
30+
31+
var flushable = sink as IFlushableFileSink;
32+
if (flushable != null)
33+
{
34+
_timer = new Timer(_ => FlushToDisk(flushable), null, flushInterval, flushInterval);
35+
}
36+
else
37+
{
38+
_timer = new Timer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
39+
SelfLog.WriteLine("{0} configured to flush {1}, but {2} not implemented", typeof(PeriodicFlushToDiskSink), sink, nameof(IFlushableFileSink));
40+
}
41+
}
42+
43+
/// <inheritdoc />
44+
public void Emit(LogEvent logEvent)
45+
{
46+
_sink.Emit(logEvent);
47+
Interlocked.Exchange(ref _flushRequired, 1);
48+
}
49+
50+
/// <inheritdoc />
51+
public void Dispose()
52+
{
53+
_timer.Dispose();
54+
(_sink as IDisposable)?.Dispose();
55+
}
56+
57+
void FlushToDisk(IFlushableFileSink flushable)
58+
{
59+
try
60+
{
61+
if (Interlocked.CompareExchange(ref _flushRequired, 0, 1) == 1)
62+
{
63+
// May throw ObjectDisposedException, since we're not trying to synchronize
64+
// anything here in the wrapper.
65+
flushable.FlushToDisk();
66+
}
67+
}
68+
catch (Exception ex)
69+
{
70+
SelfLog.WriteLine("{0} could not flush the underlying sink to disk: {1}", typeof(PeriodicFlushToDiskSink), ex);
71+
}
72+
}
73+
}
74+
}

src/Serilog.Sinks.File/Sinks/File/SharedFileSink.cs

+31-22
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,14 @@ namespace Serilog.Sinks.File
2727
/// <summary>
2828
/// Write log events to a disk file.
2929
/// </summary>
30-
public sealed class SharedFileSink : ILogEventSink, IDisposable
30+
public sealed class SharedFileSink : ILogEventSink, IFlushableFileSink, IDisposable
3131
{
3232
readonly MemoryStream _writeBuffer;
3333
readonly string _path;
3434
readonly TextWriter _output;
3535
readonly ITextFormatter _textFormatter;
3636
readonly long? _fileSizeLimitBytes;
3737
readonly object _syncRoot = new object();
38-
readonly FileInfo _fileInfo;
3938

4039
// The stream is reopened with a larger buffer if atomic writes beyond the current buffer size are needed.
4140
FileStream _fileOutput;
@@ -53,11 +52,13 @@ public sealed class SharedFileSink : ILogEventSink, IDisposable
5352
/// <returns>Configuration object allowing method chaining.</returns>
5453
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
5554
/// <exception cref="IOException"></exception>
56-
public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding encoding = null)
55+
public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes,
56+
Encoding encoding = null)
5757
{
5858
if (path == null) throw new ArgumentNullException(nameof(path));
5959
if (textFormatter == null) throw new ArgumentNullException(nameof(textFormatter));
60-
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative");
60+
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0)
61+
throw new ArgumentException("Negative value provided; file size limit must be non-negative");
6162

6263
_path = path;
6364
_textFormatter = textFormatter;
@@ -72,20 +73,16 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL
7273
// FileSystemRights.AppendData sets the Win32 FILE_APPEND_DATA flag. On Linux this is O_APPEND, but that API is not yet
7374
// exposed by .NET Core.
7475
_fileOutput = new FileStream(
75-
path,
76+
path,
7677
FileMode.Append,
7778
FileSystemRights.AppendData,
7879
FileShare.ReadWrite,
7980
_fileStreamBufferLength,
8081
FileOptions.None);
8182

82-
if (_fileSizeLimitBytes != null)
83-
{
84-
_fileInfo = new FileInfo(path);
85-
}
86-
8783
_writeBuffer = new MemoryStream();
88-
_output = new StreamWriter(_writeBuffer, encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
84+
_output = new StreamWriter(_writeBuffer,
85+
encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
8986
}
9087

9188
/// <summary>
@@ -96,20 +93,14 @@ public void Emit(LogEvent logEvent)
9693
{
9794
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
9895

99-
if (_fileSizeLimitBytes != null)
100-
{
101-
if (_fileInfo.Length >= _fileSizeLimitBytes.Value)
102-
return;
103-
}
104-
10596
lock (_syncRoot)
10697
{
10798
try
10899
{
109100
_textFormatter.Format(logEvent, _output);
110101
_output.Flush();
111102
var bytes = _writeBuffer.GetBuffer();
112-
var length = (int)_writeBuffer.Length;
103+
var length = (int) _writeBuffer.Length;
113104
if (length > _fileStreamBufferLength)
114105
{
115106
var oldOutput = _fileOutput;
@@ -126,6 +117,16 @@ public void Emit(LogEvent logEvent)
126117
oldOutput.Dispose();
127118
}
128119

120+
if (_fileSizeLimitBytes != null)
121+
{
122+
try
123+
{
124+
if (_fileOutput.Length >= _fileSizeLimitBytes.Value)
125+
return;
126+
}
127+
catch (FileNotFoundException) { } // Cheaper and more reliable than checking existence
128+
}
129+
129130
_fileOutput.Write(bytes, 0, length);
130131
_fileOutput.Flush();
131132
}
@@ -143,11 +144,19 @@ public void Emit(LogEvent logEvent)
143144
}
144145
}
145146

146-
/// <summary>
147-
/// Performs application-defined tasks associated with freeing, releasing, or
148-
/// resetting unmanaged resources.
149-
/// </summary>
147+
148+
/// <inheritdoc />
150149
public void Dispose() => _fileOutput.Dispose();
150+
151+
/// <inheritdoc />
152+
public void FlushToDisk()
153+
{
154+
lock (_syncRoot)
155+
{
156+
_output.Flush();
157+
_fileOutput.Flush(true);
158+
}
159+
}
151160
}
152161
}
153162

src/Serilog.Sinks.File/Sinks/File/WriteCountingStream.cs

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public override void Write(byte[] buffer, int offset, int count)
5151
public override bool CanWrite => true;
5252
public override long Length => _stream.Length;
5353

54+
5455
public override long Position
5556
{
5657
get { return _stream.Position; }

src/Serilog.Sinks.File/project.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "3.0.2-*",
2+
"version": "3.1.0-*",
33
"description": "Write Serilog events to a text file in plain or JSON format.",
44
"authors": [ "Serilog Contributors" ],
55
"packOptions": {
@@ -9,7 +9,7 @@
99
"iconUrl": "http://serilog.net/images/serilog-sink-nuget.png"
1010
},
1111
"dependencies": {
12-
"Serilog": "2.2.0"
12+
"Serilog": "2.3.0"
1313
},
1414
"buildOptions": {
1515
"keyFile": "../../assets/Serilog.snk",
@@ -24,7 +24,8 @@
2424
"System.IO": "4.1.0",
2525
"System.IO.FileSystem": "4.0.1",
2626
"System.IO.FileSystem.Primitives": "4.0.1",
27-
"System.Text.Encoding.Extensions": "4.0.11"
27+
"System.Text.Encoding.Extensions": "4.0.11",
28+
"System.Threading.Timer": "4.0.1"
2829
}
2930
}
3031
}

0 commit comments

Comments
 (0)