Skip to content

Commit 6f15ef5

Browse files
committed
Fixes serilog#18 - flush to disk option; re-enables (and fixes) shared file sink tests.
1 parent 3384cde commit 6f15ef5

11 files changed

+197
-43
lines changed

src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs

+30-8
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));
@@ -192,12 +197,28 @@ static LoggerConfiguration ConfigureFile(
192197
#if ATOMIC_APPEND
193198
if (shared)
194199
{
195-
sink = new SharedFileSink(path, formatter, fileSizeLimitBytes);
200+
var sfs = new SharedFileSink(path, formatter, fileSizeLimitBytes);
201+
if (flushToDiskInterval.HasValue)
202+
{
203+
sink = new PeriodicFlushToDiskSink<SharedFileSink>(sfs, flushToDiskInterval.Value);
204+
}
205+
else
206+
{
207+
sink = sfs;
208+
}
196209
}
197210
else
198211
{
199212
#endif
200-
sink = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered);
213+
var fs = new FileSink(path, formatter, fileSizeLimitBytes, buffered: buffered);
214+
if (flushToDiskInterval.HasValue)
215+
{
216+
sink = new PeriodicFlushToDiskSink<FileSink>(fs, flushToDiskInterval.Value);
217+
}
218+
else
219+
{
220+
sink = fs;
221+
}
201222
#if ATOMIC_APPEND
202223
}
203224
#endif
@@ -212,6 +233,7 @@ static LoggerConfiguration ConfigureFile(
212233
return addSink(new NullSink(), LevelAlias.Maximum, null);
213234
}
214235

236+
215237
return addSink(sink, restrictedToMinimumLevel, levelSwitch);
216238
}
217239
}

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,66 @@
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+
/// <typeparam name="TSink">The type of the wrapped sink.</typeparam>
13+
public class PeriodicFlushToDiskSink<TSink> : ILogEventSink, IDisposable
14+
where TSink : ILogEventSink, IFlushableFileSink
15+
{
16+
readonly TSink _sink;
17+
readonly Timer _timer;
18+
int _flushRequired;
19+
20+
/// <summary>
21+
/// Construct a <see cref="PeriodicFlushToDiskSink{TSink}"/> that wraps
22+
/// <paramref name="sink"/> and flushes it at the specified <paramref name="flushInterval"/>.
23+
/// </summary>
24+
/// <param name="sink">The sink to wrap.</param>
25+
/// <param name="flushInterval">The interval at which to flush the underlying sink.</param>
26+
/// <exception cref="ArgumentNullException"></exception>
27+
public PeriodicFlushToDiskSink(TSink sink, TimeSpan flushInterval)
28+
{
29+
if (sink == null) throw new ArgumentNullException(nameof(sink));
30+
31+
_sink = sink;
32+
_timer = new Timer(_ => FlushToDisk(), null, flushInterval, flushInterval);
33+
}
34+
35+
/// <inheritdoc />
36+
public void Emit(LogEvent logEvent)
37+
{
38+
_sink.Emit(logEvent);
39+
Interlocked.Exchange(ref _flushRequired, 1);
40+
}
41+
42+
/// <inheritdoc />
43+
public void Dispose()
44+
{
45+
_timer.Dispose();
46+
(_sink as IDisposable)?.Dispose();
47+
}
48+
49+
void FlushToDisk()
50+
{
51+
try
52+
{
53+
if (Interlocked.CompareExchange(ref _flushRequired, 0, 1) == 1)
54+
{
55+
// May throw ObjectDisposedException, since we're not trying to synchronize
56+
// anything here in the wrapper.
57+
_sink.FlushToDisk();
58+
}
59+
}
60+
catch (Exception ex)
61+
{
62+
SelfLog.WriteLine("Could not flush the underlying sink to disk: {0}", ex);
63+
}
64+
}
65+
}
66+
}

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

+3-2
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": {
@@ -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)